package org.codefilarete.stalactite.engine.configurer.dslresolver;

import java.util.ArrayDeque;
import java.util.Deque;

import org.codefilarete.stalactite.engine.configurer.dslresolver.MetadataSolvingCache.EntitySource;
import org.codefilarete.stalactite.sql.ConnectionConfiguration;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.tool.collection.KeepOrderSet;

public class RelationsMetadataResolver {
	
	private final OneToOneMetadataResolver oneToOneMetadataResolver;
	private final OneToManyMetadataResolver oneToManyMetadataResolver;
	private final ManyToManyMetadataResolver manyToManyMetadataResolver;
	private final ManyToOneMetadataResolver manyToOneMetadataResolver;
	private final MapMetadataResolver mapMetadataResolver;
	private final ElementCollectionMetadataResolver elementCollectionMetadataResolver;
	
	public RelationsMetadataResolver(Dialect dialect, ConnectionConfiguration connectionConfiguration) {
		this.oneToOneMetadataResolver = new OneToOneMetadataResolver(dialect, connectionConfiguration);
		this.oneToManyMetadataResolver = new OneToManyMetadataResolver(dialect, connectionConfiguration);
		this.manyToManyMetadataResolver = new ManyToManyMetadataResolver(dialect, connectionConfiguration);
		this.manyToOneMetadataResolver = new ManyToOneMetadataResolver(dialect, connectionConfiguration);
		this.mapMetadataResolver = new MapMetadataResolver(dialect, connectionConfiguration);
		this.elementCollectionMetadataResolver = new ElementCollectionMetadataResolver(dialect);
	}
	
	<C, I> void resolve(EntitySource<C, I> rootEntitySource) {
		
		// Non-recursive, stack-based depth-first traversal over the EntitySource tree.
		// Each element on the stack is an EntitySource to process, processing it produces
		// new EntitySources (the relation targets) which are pushed onto the stack.
		Deque<EntitySource<?, ?>> stack = new ArrayDeque<>();
		stack.push(rootEntitySource);
		
		while (!stack.isEmpty()) {
			EntitySource<?, ?> current = stack.pop();
			KeepOrderSet<EntitySource<?, ?>> childSources = resolveRelations(current);
			// Push children so they are processed in subsequent iterations (depth-first)
			for (EntitySource<?, ?> child : childSources) {
				stack.push(child);
			}
		}
	}
	
	/**
	 * Resolves all one-to-one and one-to-many relations for the given {@link EntitySource},
	 * covering both the entity's own configurations and its ancestors'.
	 *
	 * @return the set of {@link EntitySource}s produced by the resolved relations, to be enqueued for further traversal
	 */
	private <C, I, X> KeepOrderSet<EntitySource<?, ?>> resolveRelations(EntitySource<C, I> source) {
		
		KeepOrderSet<EntitySource<?, ?>> childSources = new KeepOrderSet<>();
		
		// --- Relations owned by this entity ---
		childSources.addAll(resolveForConfiguration(source));
		
		// --- Relations owned by ancestor entities ---
		source.<X>getAncestorSources()
				.forEach(entitySource ->
						childSources.addAll(resolveForConfiguration(entitySource)));
		
		return childSources;
	}
	
	/**
	 * Resolves all one-to-one and one-to-many relations (including those embedded in insets) and returns the resulting
	 * children.
	 */
	private <C, I> KeepOrderSet<EntitySource<?, ?>> resolveForConfiguration(EntitySource<C, I> source) {
		
		KeepOrderSet<EntitySource<?, ?>> newSourcesFound = new KeepOrderSet<>();
		
		// One-to-one relations directly on the entity and embedded in insets
		newSourcesFound.addAll(oneToOneMetadataResolver.resolve(source));
		// One-to-many relations directly on the entity and embedded in insets
		newSourcesFound.addAll(oneToManyMetadataResolver.resolve(source));
		// Many-to-many relations directly on the entity and embedded in insets
		newSourcesFound.addAll(manyToManyMetadataResolver.resolve(source));
		// Many-to-many relations directly on the entity and embedded in insets
		newSourcesFound.addAll(manyToOneMetadataResolver.resolve(source));
		// Map relations directly on the entity
		newSourcesFound.addAll(mapMetadataResolver.resolve(source));
		// Collection of elements' relations directly on the entity and embedded in insets
		elementCollectionMetadataResolver.resolve(source);
		
		return newSourcesFound;
	}
}
